如果你偏爱基于XML的格式,Spring也提供了"aop"命名空间标签来支持XML的定义。当使用@AspectJ风格时,支持完全相同的切点表达式和同志类型,因此在本节中,我们将关注新的语法,并请读者参考上一节中的讨论(10.2节,"@AspectJ support"),了解编写切点表达式和绑定通知参数。
为了使用这节描述的aop命名空间,你需要引入41章,XML Schema-based configuration中描述的spring-aop
。在41.2.7,"the aop schema"查看如何引入aop
命名空间。
在你的Spring配置里,全部的切面和通知元素都必须要放在<aop:config>
里(在你的应用程序上下文中配置可以有多个<aop:config>
)。一个<aop:config>
元素可以包含切点,通知,和切面元素(注意,他们必须以这样的顺序声明)。
<aop:config>
风格的配置大量的使用的Spring的auto-proxying机制。如果你已经明确通过BeanNameAutoProxyCreator
或类似的直接使用代理,可能会导致问题(比如通知没被织入)。建议的使用方式是只用<aop:config>
风格或是只用AutoProxyCreator
风格。
通过schema支持,切面可以是在Spring应用程序上下文中定义为Bean的普通Java对象。对象的字段和方法决定了状态和行为,XML决定了切点和通知。
一个切面用
<aop:config>
<aop:aspect id="myAspect" ref="aBean">
...
</aop:aspect>
</aop:config>
<bean id="aBean" class="...">
...
</bean>
当然,切面对应的bean(这里是'aBean')可以像其他Spring Bean一样被配置和依赖注入。
切点可以在
<aop:config>
<aop:pointcut id="businessService"
expression="execution(* com.xyz.myapp.service.*.*(..))"/>
</aop:config>
注意,上面的切点表示使用了我们在10.2,节,"@AspectJ support"相同的AspectJ切点表达式语言。如果你在使用基于schema的声明风格,你可以引用(@Aspects)类型内定义的切点表达式命名的切点。另一种定义上面切点的方式是:
<aop:config>
<aop:pointcut id="businessService"
expression="com.xyz.myapp.SystemArchitecture.businessService()"/>
</aop:config>
假设你已经有了我们在"Sharing common pointcut definitions"中定义的SystemArchitecture
切面。
在切面内声明切点和在顶层中声明切点是很类似的:
<aop:config>
<aop:aspect id="myAspect" ref="aBean">
<aop:pointcut id="businessService"
expression="execution(* com.xyz.myapp.service.*.*(..))"/>
...
</aop:aspect>
</aop:config>
和在@AspectJ切面内的方法大致相同,基于schma定义风格声明的切点也可以搜集连接点的上下文。比如,下面的切点在连接点上下文搜集了this
对象,并传给通知:
<aop:config>
<aop:aspect id="myAspect" ref="aBean">
<aop:pointcut id="businessService"
expression="execution(* com.xyz.myapp.service.*.*(..)) && this(service)"/>
<aop:before pointcut-ref="businessService" method="monitor"/>
...
</aop:aspect>
</aop:config>
通知必须声明相匹配的参数来接受连接点上下文搜集的参数:
public void monitor(Object service) {
...
}
'&&'在XML文档中是很尴尬的,因此当连接切点的各子表达式时,'and','or',和'not'可以用来取代'&&','||'和'!'。比如,前一个切点表达式也可以写作:
<aop:config>
<aop:aspect id="myAspect" ref="aBean">
<aop:pointcut id="businessService"
expression="execution(* com.xyz.myapp.service.*.*(..)) **and** this(service)"/>
<aop:before pointcut-ref="businessService" method="monitor"/>
...
</aop:aspect>
</aop:config>
注意,以这种方式定义的切点通常由其XMLID引用,且这样命名的切点不能组成复合切点。基于schema定义风格的切点比@AspectJ风格提供的支持更加有限。
和@AspectJ风格一样,(基于schema定义风格)也提供了五种通知类型,它们的语义完全相同。
前置通知运行在匹配的方法执行前。通过在<aop:aspect>
使用
<aop:aspect id="beforeExample" ref="aBean">
<aop:before
pointcut-ref="dataAccessOperation"
method="doAccessCheck"/>
...
</aop:aspect>
这里的dataAccessOperation
是定义在顶层(<aop:config>
)中切点的ID。如果像在行内定义一个切点。只需要用pointcut
属性取代pointcut-ref
属性:
<aop:aspect id="beforeExample" ref="aBean">
<aop:before
pointcut="execution(* com.xyz.myapp.dao.*.*(..))"
method="doAccessCheck"/>
...
</aop:aspect>
正如我们在讨论@AspectJ风格中注意到的,命名切点可以帮助我们提高代码的可读性。
通知里的method属性确定了一个方法(doAccessCheck
)。这个方法必须要在这个包含这个通知的切面引用的Bean中。在输入访问执行之前(一个符合切点表达式的方法执行连接点),切面bean的"doAccesCheck"方法将会被调用。
After returning通知在匹配的方法正常执行完后运行。它和before通知一样,声明在<aop:aspect>
内部。比如:
<aop:aspect id="afterReturningExample" ref="aBean">
<aop:after-returning
pointcut-ref="dataAccessOperation"
method="doAccessCheck"/>
...
</aop:aspect>
就如@AspectJ风格一样,它也可以在通知体内获取返回值,用returning属性来指出用来接受返回值的参数名称:
<aop:aspect id="afterReturningExample" ref="aBean">
<aop:after-returning
pointcut-ref="dataAccessOperation"
returning="retVal"
method="doAccessCheck"/>
...
</aop:aspect>
doAccessCheck方法必须要声明一个叫retVal
的参数。参数的类型和@AfterReturning中描述相同的方式约束了匹配的方法。比如方法签名可以被声明为:
public void doAccessCheck(Object retVal) {...
After throwing通知在方法抛出异常退出时执行。它通过在<aop:aspect>
属性内使用after-throwing元素声明:
<aop:aspect id="afterThrowingExample" ref="aBean">
<aop:after-throwing
pointcut-ref="dataAccessOperation"
method="doRecoveryActions"/>
...
</aop:aspect>
就像@AspectJ风格一样,它也可以在通知体内获取抛出的异常。用throwing属性指出接受异常的参数名:
<aop:aspect id="afterThrowingExample" ref="aBean">
<aop:after-throwing
pointcut-ref="dataAccessOperation"
throwing="dataAccessEx"
method="doRecoveryActions"/>
...
</aop:aspect>
doRecoveryActions方法必须声明一个名为dataAccessEx
的参数。参数的类型和@AfterThrowing一样可以约束匹配的方法。方法的签名可以被声明成这样:
public void doRecoveryActions(DataAccessException dataAccessEx) {...
无论匹配的方法如何退出,After (finally) 通知都会在其退出后运行。它用after
元素声明:
<aop:aspect id="afterFinallyExample" ref="aBean">
<aop:after
pointcut-ref="dataAccessOperation"
method="doReleaseLock"/>
...
</aop:aspect>
这种通知是一个环绕通知。环绕通知的运行保卫了匹配的方法执行。它可以在方法执行前,后执行,并决定了何时,如何,甚至根本不去执行方法。Around通知在你需要以一个线程安全的方式分享方法执行前面的状态时使用(比如打开和关闭定时器)。在其他轻量级的通知可以达到你的需求的时候先使用其他通知;如果before通知可以轻松做到,那么不要使用around通知。
Around通知用aop:around
元素声明。通知方法的第一个参数一定要是ProceedingJoinPoint
类型。通知体内,调用ProceedingJoinPoint
的proceed()
可以让执行被通知的方法。proceed
方法也可以在调用时传入一个Object[]
数组——数组中的值将会被用方法执行的参数。见the section called "Around advice"查看关于在procced时使用Object[]
的信息。
<aop:aspect id="aroundExample" ref="aBean">
<aop:around
pointcut-ref="businessService"
method="doBasicProfiling"/>
...
</aop:aspect>
doBasicProfiling
通知的实现和@AspectJ的例子完全一样(当然去掉注解):
public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
// start stopwatch
Object retVal = pjp.proceed();
// stop stopwatch
return retVal;
}
基于schema的声明风格提供了对全类型通知的支持,和@AspectJ描述的一样-通过切点的参数名匹配通知方法的参数名。详情请查看the section called"Advice parameters"。如果你希望直接指明通知方法的参数名(不依赖于之前讨论的检测策略),那么可以使用advice元素的arg-names
元素,这和the section called "Determinig argument names"描述的通知注释中的"argNames"属性以相同方式处理。例如:
<aop:before
pointcut="com.xyz.lib.Pointcuts.anyPublicMethod() and @annotation(auditable)"
method="audit"
arg-names="auditable"/>
arg-names
属性接受以逗号分隔的列表形式的参数名字。
下面的方法示例稍微涉及的基于XSD的方法,该示例演示了一些与大量强类型参数结合使用的around advice。
package x.y.service;
public interface FooService {
Foo getFoo(String fooName, int age);
}
public class DefaultFooService implements FooService {
public Foo getFoo(String name, int age) {
return new Foo(name, age);
}
}
接着是切面。注意profile(..)
方法接受的是一些强类型参数,第一个参数正好是用来调用procceed方法的连接点:这个参数表明了这是一个around advice。
package x.y;
import org.aspectj.lang.ProceedingJoinPoint;
import org.springframework.util.StopWatch;
public class SimpleProfiler {
public Object profile(ProceedingJoinPoint call, String name, int age) throws Throwable {
StopWatch clock = new StopWatch("Profiling for '" + name + "' and '" + age + "'");
try {
clock.start(call.toShortString());
return call.proceed();
} finally {
clock.stop();
System.out.println(clock.prettyPrint());
}
}
}
最后,是使得上述通知能够在特定连接点运行的XML配置:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- this is the object that will be proxied by Spring's AOP infrastructure -->
<bean id="fooService" class="x.y.service.DefaultFooService"/>
<!-- this is the actual advice itself -->
<bean id="profiler" class="x.y.SimpleProfiler"/>
<aop:config>
<aop:aspect ref="profiler">
<aop:pointcut id="theExecutionOfSomeFooServiceMethod"
expression="execution(* x.y.service.FooService.getFoo(String,int))
and args(name, age)"/>
<aop:around pointcut-ref="theExecutionOfSomeFooServiceMethod"
method="profile"/>
</aop:aspect>
</aop:config>
</beans>
如果我们有下面这样的驱动脚本(启动入口),我们将得到像下面这样的标准输出:
import org.springframework.beans.factory.BeanFactory;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import x.y.service.FooService;
public final class Boot {
public static void main(final String[] args) throws Exception {
BeanFactory ctx = new ClassPathXmlApplicationContext("x/y/plain.xml");
FooService foo = (FooService) ctx.getBean("fooService");
foo.getFoo("Pengo", 12);
}
}
StopWatch 'Profiling for 'Pengo' and '12'': running time (millis) = 0
-----------------------------------------
ms % Task name
-----------------------------------------
00000 ? execution(getFoo)
如果许多通知需要在相同的连接点上执行,那么执行顺序的规则和"Advice ordering"这节中讨论的一样。切面的优先级可以通过在切面对应的bean上添加Order
注解或是直接让bean实现Orderded
接口决定。
引入(在AspectJ中称为类型间声明)使得切面可以让被通知的对象实现给定的接口,并且提供这个接口的实现来代理接口的行为。
引入可以通过aop:aspect
中的aop:declare-parents
元素来实现。这个元素用来声明匹配的配型有一个新的父级(正如名字)。比如,一个接口UsageTracked
,以及这个接口的默认实现DefaultUsageTracked
,下面的切面声明了service接口的全部实现都实现了UsageTracked
接口。(比如为了通过JMX传递出采集信息)。
<aop:aspect id="usageTrackerAspect" ref="usageTracking">
<aop:declare-parents
types-matching="com.xzy.myapp.service.*+"
implement-interface="com.xyz.myapp.service.tracking.UsageTracked"
default-impl="com.xyz.myapp.service.tracking.DefaultUsageTracked"/>
<aop:before
pointcut="com.xyz.myapp.SystemArchitecture.businessService()
and this(usageTracked)"
method="recordUsage"/>
</aop:aspect>
usageTracking
这个bean代表的类包含下面这个方法:
public void recordUsage(UsageTracked usageTracked) {
usageTracked.incrementUseCount();
}
需要实现的接口是根据implement-interface
决定的。types-matching
属性的值是一个Aspectj类型的样式:任何匹配这个类型的bean都将实现UsageTracked
接口。注意,在上面before advice的例子中,service beans可以直接被用作UsageTracked
接口的实现。如果通过编码的方式访问bean,你可以这么写:
UsageTracked usageTracked = (UsageTracked) context.getBean("myService");
schema定义的切面支持单例的初始化模式。其他初始化模式可能会在将来的版本中支持。
"advisors"的概念是从Spring 1.2中被引入AOP并且AspectJ没有等价的概念。一个advisor像是一个只包含自己一个通知的切面。通知本身代表了一个bean,并且必须实现12.3.2节,"Advice types in Spring"中描述的通知接口的其中一个接口。Advisor可以使用AspectJ切点表达式。
Srping通过<aop:advisor>
元素对advisor提供了支持。最常见的是它与事务通知结合使用,事务通知在Spring中也有自己的命名空间。来看一下:
<aop:config>
<aop:pointcut id="businessService"
expression="execution(* com.xyz.myapp.service.*.*(..))"/>
<aop:advisor
pointcut-ref="businessService"
advice-ref="tx-advice"/>
</aop:config>
<tx:advice id="tx-advice">
<tx:attributes>
<tx:method name="*" propagation="REQUIRED"/>
</tx:attributes>
</tx:advice>
除了上述例子中用到的pointcut-ref
属性,你也可以使用pointcut
属性在行内定义切点表达式。
为了定义advisor的优先级,让通知按顺序执行,可以使用order
属性来定义advisor的Ordered
值。
让我们来看一下怎么用schema的方式重写11.2.7节,"Example"中的并发锁失败重试的例子。
业务服务有时候会因为并发的问题导致执行失败。如果操作重试的话,则很有可能会在下一次成功。对于适合在这些条件下(比如不需要将冲突解决的信息返回给用户的idempotent操作)重试的业务服务,我们希望透明的处理重试操作避免客户端看到PessimisticLockingFailureExcption
。这需要在service层横切好几个服务。因此适合用切面来实现。
因为我们希望重试操作,我们需要使用around advice让我们来proceed数次。这是一个基础的切面实现(使用schema的话,这只是个普通的Java类):
public class ConcurrentOperationExecutor implements Ordered {
private static final int DEFAULT_MAX_RETRIES = 2;
private int maxRetries = DEFAULT_MAX_RETRIES;
private int order = 1;
public void setMaxRetries(int maxRetries) {
this.maxRetries = maxRetries;
}
public int getOrder() {
return this.order;
}
public void setOrder(int order) {
this.order = order;
}
public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable {
int numAttempts = 0;
PessimisticLockingFailureException lockFailureException;
do {
numAttempts++;
try {
return pjp.proceed();
}
catch(PessimisticLockingFailureException ex) {
lockFailureException = ex;
}
} while(numAttempts <= this.maxRetries);
throw lockFailureException;
}
}
注意切面实现了Ordered
接口,因此我们可以设置切面的优先级比事务的优先级高(我们希望每次重试都是一个新事务)。maxRetries
和order
属性都可以通过Spring配置。主要的通知行为发生在doConcurrentOperation
中。当我们尝试proced时如果因为PessimisticLockingFailureExcption
失败,只要重试次数没用尽,我们就可以很简单再吃尝试。
这个类和@AspectJ例子中使用的是一致的,只不过移除了注解。
对应的Spring配置:
<aop:config>
<aop:aspect id="concurrentOperationRetry" ref="concurrentOperationExecutor">
<aop:pointcut id="idempotentOperation"
expression="execution(* com.xyz.myapp.service.*.*(..))"/>
<aop:around
pointcut-ref="idempotentOperation"
method="doConcurrentOperation"/>
</aop:aspect>
</aop:config>
<bean id="concurrentOperationExecutor"
class="com.xyz.myapp.service.impl.ConcurrentOperationExecutor">
<property name="maxRetries" value="3"/>
<property name="order" value="100"/>
</bean>
注意我们假设所有的服务都是idempotent。如果不是,我们定义一个切面只重试idempotent操作,通过添加一个Idempotent
注解:
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
// marker annotation
}
并且将注解应用到service 操作上。通过重定义切点表达式让它只匹配有@Idempotent
的操作我们可以让切面很容易的变成只重试idempotent的操作:
<aop:pointcut id="idempotentOperation"
expression="execution(* com.xyz.myapp.service.*.*(..)) and
@annotation(com.xyz.myapp.service.Idempotent)"/>